對,是系列文!
在 Vue 中,組件之間傳遞資料的機制有許多種,本系列主要介紹父、子組件之間的溝通,分別為:「父傳子:Props」&&「子傳父:Emits」。
這兩篇都是學習使用 <script setup>
的語法糖喔,官方文件另有提供非此語法的寫法,大家可以再爬爬。
提供一下今天的小摘要:
- Props 定義
- defineProps() 語法
- Props 命名格式建議
- 靜態 vs. 動態 Props
- 傳遞不同的值類型
Number
Boolean
Array
Object- 使用一個對象綁定多個 props
- 單向數據流
- Prop 校驗
- Boolean 類型轉換
我們~~開始嚕!
官方文件:Props 是一種特別的 attributes,你可以在組件上聲明註冊。
Props 是一種 從父組件傳遞資料給子組件 的機制。
這個機制透過屬性(attributes)來表現,而子組件能夠接收父組件傳遞的資料。
具體的操作如下:
defineProps()
語法來定義需要「接收資料」的屬性(props)。讓子組件明確知道自己會接收哪些資料,並且可以在組件內部使用這些 props。設置要「接收資料」的屬性。
可以用「這個屬性是一個接口,會接住從父組件傳來的資料」來理解。
注意這邊:陣列中的元素必須為字串喔。
<script setup>
defineProps(['你要定義的 props 屬性']);
</script>
可指定類型、預設值、是否作為必要參數。
defineProps({
你要定義的 props 屬性: 指定的類型;
})
// 定義的屬性內可以再包一個物件指定相關訊息
defineProps({
你要定義的 props 屬性: {
// ... 可指定類型、預設值、是否作為必要參數。
type: String, // 指定類型
default: 'default value', // 預設值
required: true // 是否必要
}
})
defineProps()
語法有幾種特性可以注意:
馬上看一下子組件的使用範例:
<script setup>
// 1. 不需要 import 就可以使用。
const props = defineProps(["message"]);
// 2. 會 return 一個物件,其中包含定義的所有參數。
console.log(props);
</script>
<template>
<h3>子組件在這邊可以接到:{{ message }}</h3>
<!-- 3. 參數可以直接在模板中使用。 -->
</template>
看一下在瀏覽器中 2. log
出來的樣子 和 3. 模板中使用的狀況:
(第 4 點我們稍後會說明~)
defineProps()
是一種只能在 <script setup>
中使用的編譯宏,編譯宏是指在編譯器編譯時,會將傳入的參數轉為文字替換值,而不會對其做表達式的求值或類型檢查的操作。
在這邊就是將 ()
中的值做了文本替換,暴露到模板,是一種預處理的操作~
<script setup>
const props = defineProps({
myPropsData: String // 使用 camelCase 命名方式
});
</script>
<template>
<div>{{ myPropsData }}</div>
</template>
<MyComponent my-props-data="Hihi" />
靜態是指:在父組件綁定時,不用 v-bind
作為綁定方式,將屬性值作為「字串」傳遞。
// child.vue
<script setup>
defineProps(["message"]);
</script>
<template>
<h3>子組件在這邊可以接到:{{ message }}</h3>
</template>
// parent.vue
<script setup>
import Child from "./child.vue";
</script>
<template>
<Child message="從父組件定義的資料!" />
</template>
我們沿著剛剛提到的 具體操作要點 來檢視這個範例!
defineProps(["message"]);
定義了屬性 message
,而 message
屬性可以於模板中直接使用。import Child from "./Child.vue";
導入子組件,並在模板中以 <Child />
使用子組件。<Child message="從父組件定義的資料!" />
綁定 props 屬性為 message
,並定義資料。因此 message
這個 props 會將 "從父組件定義的資料!"
傳遞到子組件,子組件將這個資料顯示在模板的 <h3>
中。
瀏覽器上的呈現:
用圖來理解一下~!
動態是指:在父組件綁定時,用 v-bind
作為綁定方式,將屬性值作為「表達式」傳遞。
我們把上面的例子優化成動態的綁定試試看,將父組件的傳入的資料綁定為一個「響應式狀態」。
// parentActive.vue
<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
const data = ref([
{
id: 1,
title: "第一個內容",
},
{
id: 2,
title: "第二個內容",
},
{
id: 3,
title: "第三個內容",
},
]);
</script>
<template>
<Child
v-for="dataItem in data"
:key="dataItem.id"
:message="dataItem.title"
/>
</template>
這邊我們將父組件傳遞的資料定義為 data
這個響應式陣列,而在模板中,用 v-for
渲染資料、指定 key
屬性:
<template>
<Child
v-for="dataItem in data"
:key="dataItem.id"
:message="dataItem.title"
/>
</template>
看看父組件怎麼傳遞資料:
父組件 import 了子組件,並將 <Child :message="dataItem.title />
使用 :
動態綁定 :message
為 props
屬性,而屬性值為 data
中的每個物件的 title
。
因此 第一個內容
、第二個內容
、第一三內容
會由 message
屬性傳遞到子組件,將依序渲染,顯示在模板的 <h3>
中。
我們剛剛有提到 defineProps()
的參數默認接受所有的數據類型。
而其可以涵蓋以下類型:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
- Error
- constructor
接下來我們會以幾種參數作為範例~
註:以下類型雖然皆可以為字串形式傳遞,但記得我們要傳入綁定的是一個「表達式」,所以建議使用 :
動態綁定。
傳入 Number 時,使用 :
動態綁定,因為要傳入的是表達式不是字串。
// childNumber.vue
<script setup>
defineProps(["message"]);
</script>
<template>
<h3>子組件在這邊可以接到:{{ message }}</h3>
</template>
// parentNumber.vue
<script setup>
import { reactive } from "vue";
import Child from "./childNumber.vue";
const data = reactive({
number: 100,
});
</script>
<template>
<Child :message="100" />
<Child :message="data.number" /> // 動態綁定 data 響應式物件的屬性
</template>
看看瀏覽器上呈現:
傳入布林值時,使用 :
動態綁定,因為要傳入的是表達式不是字串。
// childBoolean.vue
<script setup>
const props = defineProps(["isShow"]);
console.log(props);
</script>
<template>
<div>我被渲染出來了:{{ isShow }}</div>
</template>
// parentBoolean.vue
<script setup>
import { ref } from "vue";
import Child from "./childBoolean.vue";
const data = ref(true);
</script>
<template>
<Child isShow />
<Child :isShow="true" />
<Child :isShow="data" />
</template>
可以關注一下右邊,傳給子組件的 props 是 Proxy 物件(趁機打廣告)。
而在第一個子組件中,我們使用了未綁定值的 <Child isShow />
傳遞資料,結果渲染成了空白!
這是因為:當 props 沒有指定值時,預設會被轉為 true
。但!也因為在子組件中並沒有明確將 isShow
定義為 type: Boolean
,到文本插值中就轉為了 ''
空字串(稍後會說明 Boolean 的 props 值轉換~)
使用 :
動態綁定。
// childArray.vue
<script setup>
defineProps(["message"]);
</script>
<template>
<div>我被渲染出來了:{{ message.join(",") }}</div>
</template>
// parentArray.vue
<script setup>
import { ref } from "vue";
import Child from "./childArray.vue";
const data = ref(["data1", "data2", "data3"]);
</script>
<template>
<Child :message="['data1', 'data2', 'data3']" />
<Child :message="data" />
</template>
這邊可以先注意一下,子組件接收陣列資料,可對其做 .join()
操作,因為 join()
會 return 新的陣列,而不會改動到原資料(稍後我們會講到資料的唯讀特性)。
使用 :
動態綁定。
// childObject
<script setup>
defineProps(["id", "title", "number"]);
</script>
<template>
<div>我的 id:{{ id }}</div>
<div>我的 title:{{ title }}</div>
<div>我的 number:{{ number }}</div>
</template>
子組件中定義了三個屬性。
// parentObject. vue
<script setup>
import { reactive } from "vue";
import Child from "./childObject.vue";
const data = reactive({
id: 1,
title: "第一個內容",
number: 100,
});
</script>
<template>
<Child :id="data.id" :title="data.title" :number="data.number" />
</template>
這邊分別用了 :id
、:title
、:number
動態綁定 props 屬性,值對應到的是 data
這個響應式物件中的值!
物件可以綁定多個 props 屬性,我們可以使用「沒有帶參數的 v-bind
」 實現。
來優化一下剛剛物件的範例!
// childObject.vue
<script setup>
defineProps(["id", "title", "number"]);
</script>
<template>
<div>我的 id:{{ id }}</div>
<div>我的 title:{{ title }}</div>
<div>我的 number:{{ number }}</div>
</template>
// parentObject.vue
<script setup>
import { reactive } from "vue";
import Child from "./childObject.vue";
const data = reactive({
id: 1,
title: "第一個內容",
number: 100,
});
</script>
<template>
<Child :id="data.id" :title="data.title" :number="data.number" />
<Child v-bind="data" />
// 這邊可以直接傳遞 data 的所有屬性到子組件
</template>
父組件我們使用了兩種寫法傳遞參數:
<Child :id="data.id" :title="data.title" :number="data.number" />
一般的 :
動態綁定。<Child v-bind="data" />
則是使用「沒有帶參數的 v-bind
」 直接傳入一個表達式,其中響應式的物件的「所有屬性」都會自動轉換為子組件的 props(只能說超讚的)。渲染結果是一樣的哦!
是 props 的資料傳遞的原則!
看看官方文件怎麼說:
所有的 props 都遵循著單向綁定原則,props 因父組件的更新而變化,自然地將新的狀態向下流往子組件,而不會逆向傳遞。
這避免了子組件意外修改父組件的狀態的情況,不然應用的數據流將很容易變得混亂而難以理解。
因此 Props 的資料傳遞 會保持以下準則:
我們可以來看看 3. 修改屬性的情況。
如果我們在子組件上修改:
<script setup>
defineProps(["message"]);
</script>
<template>
<h3>子組件在這邊可以接到:{{ message++ }}</h3>
// 在此對父組件傳進來的資料做變更
</template>
會拋出唯讀的警吿:
但我們需要修改這個值的話,有下列兩個方式:
我們以這個例子來看,父組件傳入了 message
屬性,但我們將其傳入 defaultProps
,並在點擊按鈕觸發後修改它的值。
<script setup>
import { ref } from "vue";
const props = defineProps(["message"]);
const defaultProps = ref(props.message);
function messageUpdate() {
defaultProps.value = "從父組件定義的資料被修改了!";
}
</script>
<template>
<h3>子組件在這邊可以接到:{{ defaultProps }}</h3>
<button @click="messageUpdate">增值</button>
</template>
這樣的方式就不會去更動到 props 屬性的唯讀性。
computed
函式。<script setup>
import { computed } from "vue";
const props = defineProps(["message"]);
const propsComputed = computed(() => {
return "從父組件定義的資料被 computed 修改了!";
});
</script>
<template>
<h3>子組件在這邊可以接到:{{ propsComputed }}</h3>
</template>
props 以物件、陣列形式傳遞時,無法修改其綁定,但是可以修改內部的值,因為傳遞的基礎是建立在記憶體指向。
但是~這樣會影響效能、並且違反了基礎的單向資料流的概念,官方提醒並不建議這樣做喔。
定義 props 時,明確定義資料的傳遞要求,可以幫助我們你同事了解傳遞這些 props 時的資料類型、預設值,以及是否必要。
注意:傳遞的資料若不符合定義,瀏覽器會拋出警告,但不會報錯無法運行哦。
<script setup>
defineProps({
message: Boolean,
// 傳入的值類型設為:布林值
});
</script>
<template>
<div>{{ message }}</div>
</template>
<script setup>
import { ref } from "vue";
import ChildType from "./childType.vue";
const messageProps = ref(true);
const messagePropsIncorrect = ref("notBoolean");
</script>
<template>
<ChildType :message="messageProps" />
<!-- 傳入布林值 -->
<ChildType :message="messagePropsIncorrect" />
<!-- 傳入字串 -->
</template>
瀏覽器拋出警告:<ChildType message="notBoolean" >
傳入的值非布林值。
<script setup>
defineProps({
message: [Boolean, Number, null],
// 傳入的值類型設為:布林值、數字、也可以是 null
});
</script>
<template>
<div>{{ message }}</div>
</template>
值為:true or false。
<script setup>
defineProps({
message: {
type: Boolean,
required: true,
// 必要傳入
},
});
</script>
<template>
<div>子組件在這邊可以接到:{{ message }}</div>
</template>
若父組件有 傳入布林值 和 不傳入值 的情況:
<script setup>
import { ref } from "vue";
import ChildRequire from "./childRequire.vue";
const messageProps = ref(true);
</script>
<template>
<ChildRequire :message="messageProps" />
<!-- 傳入布林值 -->
<ChildRequire />
<!-- 不傳入值 -->
</template>
瀏覽器上的結果會對 <ChildRequire />
拋出警告:
以下以 Number 作為範例:
<script setup>
defineProps({
message: {
type: Number,
default: 1000,
// 設置默認值為 1000
},
});
</script>
<template>
<div>子組件在這邊可以接到:{{ message }}</div>
</template>
<script setup>
import { ref } from "vue";
import ChildDefault from "./childDefault.vue";
const messageProps = ref(10);
const messagePropsUndefined = ref(undefined);
const messagePropsNull = ref(null);
</script>
<template>
<ChildDefault :message="messageProps" />
<ChildDefault />
<!-- 不傳入值 -->
<ChildDefault :message="messagePropsUndefined" />
<!-- 傳入 undefined -->
<ChildDefault :message="messagePropsNull" />
<!-- 傳入 null -->
</template>
瀏覽器上的呈現:
以下以 物件 來作為範例,先看一下操作的語法:
default(非必要參數) {
return 預設操作;
}
// 子組件
<script setup>
const props = defineProps({
message: {
type: Object,
// 會將父組件傳入的 props 資料當作參數 rawProps,並 return 一個預設值
default(rawProps) {
return { title: "子組件給的預設值" };
},
},
});
console.log(props);
</script>
<template>
<div>子組件在這邊可以接到:{{ message.title }}</div>
</template>
// 父組件
<script setup>
import { reactive } from "vue";
import ChildDefaultObject from "./childDefaultObject.vue";
const messagePropsObject = reactive({
message: { title: "父組件原本的資料" },
});
</script>
<template>
<ChildDefaultObject v-bind="messagePropsObject" />
<ChildDefaultObject />
</template>
看看瀏覽器上的呈現:
我們這邊在父組件中使用了兩個子組件,分別印出它們的 props
和 props.message
(props 屬性的值)檢視一下:
<ChildDefaultObject v-bind="messagePropsObject" />
:message: { title: "父組件原本的資料" }
,是一個「帶有 message
屬性的響應式物件」,message
的值為 { title: "父組件原本的資料" }
,因此渲染出了 "父組件原本的資料"
。<ChildDefaultObject />
:message
屬性 ...
的響應式物件」,而其中 message
的值變成了我們設置的 default 值 { title: "子組件給的預設值" }
,因此渲染出了 "子組件給的預設值"
。先看一下操作的語法:
default(非必要參數) {
return 預設操作;
}
(又踩坑了,大家一起來看!)
// 子組件
<script setup>
const props = defineProps({
message: {
type: Function,
default() {
return "預設的函式";
},
},
});
console.log(`props: `, props);
console.log(`props.message: `, props.message);
</script>
<template>
<div>子組件在這邊可以接到:{{ message }}</div>
</template>
子組件中定義了 default() { return "預設的函式"; }
其中 return 一個字串 "預設的函式"
。
// 父組件
<script setup>
import ChildDefaultFunction from "./childDefaultFunction.vue";
const messageFunction = function () {
return "父組件傳來的資料我是一個函式";
};
</script>
<template>
<ChildDefaultFunction :message="messageFunction" />
<ChildDefaultFunction />
</template>
看看瀏覽器上呈現:
怎麼怪怪的⋯⋯
由於!我們接到的 props
是函式本身,將其以 {{ message }}
渲染的話,呈現會是函式的字。
讓我們改為 {{ message() }}
呼叫它!
(這樣才對麻)
先看一下操作的語法:
validator(父組件傳遞進來的 props 值) {
return ["字定義預設值", "自定義預設值"].includes(父組件傳遞進來的 props 值);
}
看範例!
// 子組件
<script setup>
const props = defineProps({
message: {
type: String,
validator(value) {
// 驗證的值必須是這兩個字串之一
return ["預設值一", "預設值二"].includes(value);
},
},
});
</script>
<template>
<div>子組件在這邊可以接到:{{ message }}</div>
</template>
// 父組件
<script setup>
import ChildDefaultValidator from "./childDefaultValidator.vue";
</script>
<template>
<ChildDefaultValidator message="不是你的預設值" />
<ChildDefaultValidator message="預設值一" />
<ChildDefaultValidator message="預設值二" />
</template>
父組件傳入了 不是你的預設值
、預設值一
和 預設值二
這三個值,而子組件接收到時,會進入 validator
做驗證,當未符合其中定義的兩個預設值就會拋出警告。
瀏覽器上的呈現:
在 type 定義為 null
但沒有使用陣列語法時,會允許任何類型。
defineProps({
message: [Boolean, Number, null],
// 傳入的值類型設為:布林值、數字、也可以是 null
});
defineProps({
message: null,
// 允許任何類型
});
官方文件:為了更貼近原生 boolean attributes 的行為,聲明為 Boolean 類型的 props 有特別的類型轉換規則。
當預設值設置為 Boolean,但沒有使用 v-bind
綁定、未傳入值,會默認傳入的是 true
。
// 子組件
<script setup>
const props = defineProps({
isShow: Boolean,
});
</script>
<template>
<div v-if="isShow">子組件在這邊可以接到:{{ isShow }}</div>
</template>
// 父組件
<script setup>
import ChildDefaultBoolean from "./childDefaultBoolean.vue";
</script>
<template>
<ChildDefaultBoolean isShow />
// 未傳入值
</template>
瀏覽器上呈現:
當我們同時允許 String
和 Boolean
時,兩者於陣列中順序前後會有不同情況。
Boolean
於 String
前,默認為 ture
:<script setup>
const props = defineProps({
isShow: [Boolean, String],
});
console.log(`props.isShow: `, props.isShow);
</script>
<template>
<div v-if="isShow">子組件在這邊可以接到:{{ isShow }}</div>
</template>
String
於 Boolean
前,會轉為空字串
:<script setup>
const props = defineProps({
isShow: [String, Boolean],
});
console.log(`props: `, props);
console.log(`props.isShow: `, props.isShow);
</script>
<template>
<div v-if="isShow">子組件在這邊可以接到:{{ isShow }}</div>
</template>
呼!(必須喘口氣)
傳遞資料的概念好像不難理解,但用法和細節非常驚人⋯⋯有點期待之後會怎麼用到它的 XD
明天我們就來看看 Emits 又是怎麼傳遞的八~
(看來是沒有要饒過小腦袋)
https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day28